package de.dopichaj.labrador.search.lucene;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.apache.log4j.Logger;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.Token;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.PhraseQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;

import de.dopichaj.labrador.search.QueryCategory;
import de.dopichaj.labrador.search.QueryParseException;

import de.dopichaj.labrador.search.NEXIParser;

import antlr.RecognitionException;
import antlr.TokenStreamException;

/**
 * Store the queries.
 */
public class LuceneQuery implements de.dopichaj.labrador.search.Query {

    /**
     * A logger for this class
     */
    private static final Logger log = Logger.getLogger(LuceneQuery.class);
    
    /**
     * Hashtable with categories used as keys and queries as values.
     */
    private Map<QueryCategory, BooleanQuery> queries;

    /**
     * A map from the category names to the categories so that we only
     * have one instance of each category.
     */
    private final Map<String, QueryCategory> categories; 

    private final Analyzer analyzer;

    private int categoryIndex;

    private BooleanQuery targetQuery;

    /**
     * Initializes the query.
     * @throws QueryParseException 
     */
    public LuceneQuery(Analyzer analyzer, final String nexi) throws QueryParseException  {
        queries = new HashMap<QueryCategory, BooleanQuery>();
        categories = new TreeMap<String, QueryCategory>();
        createCategory("", 0.25);
        this.analyzer = analyzer;
        categoryIndex = 0;
        
        targetQuery = new BooleanQuery();
        queries.clear();
        de.dopichaj.labrador.nexi.NEXIParser parser = new de.dopichaj.labrador.nexi.NEXIParser();
        NEXIParser converter = new NEXIParser(this);
        try {
            converter.query(parser.parse(nexi));
        } catch (RecognitionException e) {
            throw new QueryParseException("Error parsing query: " + e.getMessage());
        } catch (TokenStreamException e) {
            throw new QueryParseException("Error parsing query: " + e.getMessage());
        }
    }

    /**
     * Add a lucene query in the given category.
     */
    private void addLuceneQuery(String category, Query query) {
        
        category = "";
        BooleanQuery catQuery = queries.get(getCategory(category));
        if (catQuery == null) {
            catQuery = new BooleanQuery();
        }
        
        catQuery.add(query, BooleanClause.Occur.SHOULD);
        
        queries.put(getCategory(category), catQuery);
        log.info("New query in " + category + ": " + catQuery);
        
        // always add to broadest category
        //if (!category.equals("")) {
        //    addLuceneQuery("", query);
        //}
    }
    
    /**
     * Obtain a list of Lucene Terms from a String containing a word.
     * @param word the word to convert (may not be null)
     * @param field TODO
     */
    private List<Term> parseTerm(String word, String field) {
        TokenStream ts = analyzer.tokenStream("contents", new StringReader(word));
        Token tok = null;
        List<Term> result = new ArrayList<Term>(1);
        try {
            tok = ts.next();
            while (tok != null) {
                result.add(new Term(field, tok.termText()));
                tok = ts.next();
            }
        } catch (IOException e) {
            log.error("IO error reading from String?!?", e);
            throw new RuntimeException(e);
        }
        return result;
    }

    /**
     * Add a query for a single term (which may turn out to consist of several
     * Terms as far as Lucene is concerned; in that case, it is considered a phrase).
     * @param category  the category to search in
     * @param term      the term to search for (see {@link #parseTerm}))
     * @param boost     the boost to apply
     */
    public void addTermQuery(String category, String term, float boost) {
        log.info("Adding term query: " + category + "/" + term + ":" + boost);
        
        Query query = null;
        List<Term> terms = parseTerm(term, "contents");
        if (terms.size() != 0) {
            if (terms.size() == 1) {
                query = new TermQuery(terms.get(0));
                query.setBoost(boost);
            } else {
                BooleanQuery pq = new BooleanQuery();
                pq.setBoost(boost);
    
                for (Term t : parseTerm(term, "contents")) {
                    pq.add(new TermQuery(t), BooleanClause.Occur.SHOULD);
                }
                query = pq;
            }
            addLuceneQuery(category, query);
        }
    }

    /**
     * Add a query for a phrase.
     * @param category  the category to search in
     * @param phrase    a list of terms to search for (see {@link #parseTerm}))
     * @param boost     the boost to apply
     */
    public void addPhraseQuery(String category, List<String> phrase, float boost) {
        log.info("Adding phrase query: " + category + "/" + phrase + ":" + boost);
        BooleanQuery brokenPhraseQuery = new BooleanQuery();
        PhraseQuery phraseQuery = new PhraseQuery();
        phraseQuery.setBoost(boost);
        brokenPhraseQuery.setBoost(boost / 2);
        int wordCount = 0;

        for (String s : phrase) {
            
            for (Term t : parseTerm(s, "contents")) {
                wordCount++;
                phraseQuery.add(t);
                brokenPhraseQuery.add(new TermQuery(t), BooleanClause.Occur.SHOULD);
            }
        }
        
        if (wordCount > 0) {
            addLuceneQuery(category, brokenPhraseQuery);
            addLuceneQuery(category, phraseQuery);
        }
    }

    public Collection<QueryCategory> getCategories() {
        return queries.keySet();
    }
    
    public Query getQuery(QueryCategory category) {
        return queries.get(category);
    }
    
    /**
     * Get an existing category of the given name, or create it with the default
     * weight if it does not exist.
     */
    private QueryCategory getCategory(String name) {
        if (!categories.containsKey(name)) {
            createCategory(name, 1.0);
        }
        return categories.get(name);
    }
    
    private void createCategory(String name, double weight) {
        categories.put(name, new QueryCategory(name, weight, categoryIndex++));
    }

    public void setTargetPath(String path) {
        for (final Term term : parseTerm(path, "path")) {
            targetQuery.add(new TermQuery(term), BooleanClause.Occur.SHOULD);
        }
    }
    
    public Query getTargetQuery() {
        return targetQuery ;
    }
}
